package chips440.server;

import java.awt.image.BufferedImage;
import javax.swing.JOptionPane;
import java.util.*;
import java.io.FileNotFoundException;

import chips440.internals.*;
import chips440.tiles.*;
import chips440.main.Types;
import chips440.main.Cell;

/**
 * A glue layer class to keep the details of implementation away from the IO
 * layer, and the details of the rule semantics and actual playing of the game
 * away from the board.
 *
 * @author Ian Voysey
 * @version 0.1
 */
public class Engine{
    private final int SLEEP = 125;
    private final int THREAD_SLEEP = 10;
    private final int SEC_CONVERT = 1000;
    private final int LOAD_WAIT = 500;
    private int X = 0;
    private int Y = 1;

    private Types.EngineState game_state;
    private int time_left;
    private int chips_left;
    private int current_level;
    private Board current_map;

    private Thread timer_thread;
    private HashMap<Integer, LinkedList<Types.Direct>> queues;
    private HashMap<Integer, Thread> moves_threads;
    private HashMap<Integer, Player> player_list;

    /**
     * The sole constructor. This only sets up the state; init must be called
     * to start any of the timers relevant to making the game spin.
     */
    public Engine(){
        player_list = new HashMap<Integer, Player>();
        moves_threads = new HashMap<Integer, Thread>();
        queues = new HashMap<Integer, LinkedList<Types.Direct>>();
        game_state = Types.EngineState.ALIVE;
        time_left = 0;

        chips_left = 0;
        current_level = 1;
        current_map = null;


        try{
            current_map = BoardParser.parseLevel(current_level);
        } catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException ("first board load failed; can't recover");
        }

        chips_left = current_map.getMaxChip();
        time_left = current_map.getMaxTime();
    }

    /**
     * The opposite of start - stops all of the threads of the engine,
     * much like death stops the lives of humans :)
     */
    public void kill(){
        timer_thread.interrupt();
        Set<Integer> keys = moves_threads.keySet();
        for(Integer key : keys)
            moves_threads.get(key).interrupt();
    }

    /**
     * This adds a new player to the game - creates a move queue, and
     * move thread for the player.
     *
     * @param player_id the player's id
     */
    public void add_player(int player_id){
        Player player = new Player(current_map.getPlayerPosition(), player_id);
        player_list.put(player_id, player);
        queues.put(player_id, new LinkedList<Types.Direct>());
        moves_threads.put(player_id, new Thread(new EngineProcessor(player_id)));
    }

    /**
     * This removes a player from the game by just ripping out all of its
     * state.
     */
    public void del_player(int player_id){
	player_list.remove(player_id);
	queues.remove(player_id);
	moves_threads.get(player_id).interrupt();
	moves_threads.remove(player_id);
    }

    /**
     * Returns the number of currently connected players.
     */
    public int numPlayers(){
	return player_list.keySet().size();
    }

    /**
     * This function actually starts the timer thread. It should only be
     * called once the correct numer of clients (players) are connected.
     */
    public void start(){
        clearQueues();
        timer_thread = new Thread(new Timer());
        timer_thread.start();
        Set<Integer> keyset = moves_threads.keySet();
        for(Integer key : keyset){
            moves_threads.get(key).start();
        }
    }

    /**
     * fetch the player given an ID
     *
     * @param player_id an integer to identify the player; this method will
     * not function on integers that are not handed out by init
     * @return a matrix of all positions of all players with the player of
     * interest at the front.
     */
    public int[][] getPlayerPosition (int player_id){
        Set<Integer> id_set = player_list.keySet();
        int limit = id_set.size();
        int [][] pos_array = new int[2][limit];
        int position = 1;
        for(Integer id : id_set){
            int index = position;
            if(id == player_id)
                index = 0;
            Cell pos = player_list.get(id).getPosition();
            if(index < limit){
                pos_array[X][index] = pos.getX();
                pos_array[Y][index] = pos.getY();
            }
            if(index != 0)
                position++;
        }

        return pos_array;
    }


    /**
     * Returns a list of items that a player holds.
     *
     * @param player_id the id of the player of interest
     * @throws IllegalArgumentException if the player_id is not a known ID
     * @return an enum map of the items held
     */
    public Map<Types.Item, Boolean> getItems(int player_id){
	Player p = player_list.get(player_id);

	if(p == null)
	    throw new IllegalArgumentException("invalid player id number");
	else {
	    return p.getItems();
	}
    }

    /**
     * Returns true if the player with the specified ID has the specified item.
     *
     * @param player_id the id of interest. this must be an ID that was handed
     * out by init.
     * @param whichItem the item of interest
     * @throws IllegalArgumentException if the player_id is not a known ID
     * @return true iff the specified player has the specified item
     */
    public boolean hasItem(int player_id, Types.Item whichItem){
	Player p = player_list.get(player_id);

	if(p == null)
	    throw new IllegalArgumentException("invalid player id number");
	else
	    return p.hasItem(whichItem);
    }

    /**
     * one player gives an item to any other player
     *
     * @param thisItem the item to give
     * @param giver_id the player id of the item giver
     */
    public void giveItem(Types.Item thisItem, int giver_id){
	int taker_id = giver_id;
	Set<Integer> keys = player_list.keySet();
	Integer [] ids = new Integer[keys.size()];
	ids = keys.toArray(ids);
	Random r = new Random();
	while(taker_id == giver_id){
	    taker_id = ids[Math.abs(r.nextInt()) % ids.length];
	}

	Player giver = player_list.get(giver_id);
	if(!giver.hasItem(thisItem))
	    return;

	Player taker = player_list.get(taker_id);
	giver.takeItem(thisItem);
	taker.giveItem(thisItem);

	// Check for changes to be made to position
	Cell giver_pos = player_list.get(giver_id).getPosition();
	Tile tile = current_map.getTile(giver_pos.getX(), giver_pos.getY());
	Types.TileAct tile_state = tile.okToStep(giver);

	switch(tile_state){
		case GOTO:
			queues.get(giver_id).clear();
			move(((MoveTile) tile).getDirection(), giver_id);
			break;
		case KILL:
			game_state = Types.EngineState.DEAD;
	}
    }

    /**
     * fetch the remaining time, global to all players
     *
     * @return the number of seconds remaining for the level
     */
    public int getTimeLeft(){
	return time_left;
    }

    /**
     * fetch the number of remaining chips, global to all players
     *
     * @return the number of chips that must be collected before the level is
     * done
     */
    public int getChipsLeft (){
	return chips_left;
    }

    /**
     * fetch the game state
     *
     * @return the game state
     */
    public Types.EngineState getGameState (){
	return game_state;
    }

    /**
     * Returns the 9x9 matrix of tile names centered at (x,y)
     *
     * @param x the x coordinate to paint at
     * @param y the y coordinate to paint at
     * @return the correct name of the image to paint at pos
     */
    public String[][] getImage(int x, int y, int size){
	String [][] ret_val = new String[size][size];
	for(int r = 0; r < size; r++){
	    for(int c = 0; c < size; c++){
		ret_val[r][c] = this.current_map.getTile(r+x,c+y).paintSelf();
	    }
	}
	return ret_val;
    }

    /**
     * Registers a request for a particular player to move in a particular
     * direction. As a security concern, this method has no return type: the
     * player either moves or it doesn't, but it's never told back to the
     * client. The user finds out through changes in the graphical output.
     *
     * @param direction must be one of the four constants defined above
     * @param player_id must be an id handed out by init
     */
    public void move(Types.Direct direction, int player_id){
	LinkedList<Types.Direct> q = queues.get(player_id);
	if(q == null)
	    return;
        q.offer(direction);
    }

    /**
     * move on to the next level if possible, if not assume that the user won!
     *
     * (This is a very optimistic assumption, but in a casual environment, it
     * allows us to make adding and modifying maps profoundly simple.)
     */
    public void nextLevel(){
        current_level++;
        try{
            Thread.sleep(LOAD_WAIT);
        }catch(Exception e){}
        try{
            current_map = BoardParser.parseLevel(current_level);
        } catch (FileNotFoundException e){
            game_state = Types.EngineState.DEAD;
            return;
        } catch (IllegalArgumentException e){
            // no way to fix this, particularly
        }

        chips_left = current_map.getMaxChip();
        time_left = current_map.getMaxTime();

        Set<Integer> play_ids = player_list.keySet();
        for(Integer id : play_ids){
            Player p = new Player(current_map.getPlayerPosition(), id);
            player_list.put(id, p);
        }

        clearQueues();
        game_state = Types.EngineState.ALIVE;
    }

    /**
     * Time management class (ha ha). This takes care of decrementing the time
     * and loading new levels when appropriate.
     *
     * @author Ryan Hofler
     * @version 0.1
     */
    private class Timer implements Runnable{
	private int sleep_count;

	public Timer(){
	    this.sleep_count = 0;
	}

	public void run(){
	    while(game_state == Types.EngineState.ALIVE){
		if(time_left <= 0)
		    game_state = Types.EngineState.DEAD;

		try{
                    Thread.sleep(THREAD_SLEEP);
                }catch(Exception e){}
                sleep_count += THREAD_SLEEP;
		int sleep = sleep_count / SEC_CONVERT;

                if(sleep > 0){
                    time_left -= sleep;
		    sleep_count = 0;
		}

		if(game_state == Types.EngineState.WIN){
		    nextLevel();
		    sleep_count = 0;
		}
	    }
	}
    }

    /**
     * Thread management class. This encapsulates the tasks needed to be done
     * at a specific time interval (namely processing the move queue), so that
     * they may be neatly passed into a new thread.
     *
     * @author Ryan Hofler
     * @version 0.1
     */
    private class EngineProcessor implements Runnable{
	private int player_id;
	private int sleep_count;
	private LinkedList<Types.Direct> moves_queue;

	/**
	 * The sole constructor, which sets up some small pieces of state.
	 */
        public EngineProcessor(int player_id){
            this.player_id = player_id;
            this.sleep_count = 1;
	    moves_queue = queues.get(player_id);
        }

	/**
	 * The method called by the scheduler when the thread wakes up, which
	 * manages the tasks to be done.
	 */
        public void run(){
            while(game_state == Types.EngineState.ALIVE){
                if(time_left <= 0){
                    continue;
                }
                processMoveQueue();
                try{
                    Thread.sleep(THREAD_SLEEP);
                }catch(Exception e){}

		if(game_state == Types.EngineState.WIN){
		    while(game_state != Types.EngineState.ALIVE)
			;
		}
	    }
        }

        /**
         * goes through the movequeue, attempting to make each move that's
         * been requested since the last pass
         */
        private void processMoveQueue(){
            if(moves_queue.isEmpty())
                return;
	    makeMove(moves_queue.peek(), player_id);
            moves_queue.poll();
        }

	/**
	 * This method actually implements the gorey details of the semantics
	 * of moving a player. If the proposed move contradicts an invariant
	 * (you may not walk through walls, etc.) then it simply doesn't
	 * happen.
	 *
	 * @param direction the move that the player would like to move
	 * @throws IllegalArgumentException iff direction isn't one of this
	 * classes field members or a bad tilestate got into the board
	 */
        public synchronized void makeMove (Types.Direct direction, int player_id){
	    Player player = player_list.get(player_id);
	    if(player == null)
	        return;
	    Cell player_pos = player.getPosition();

	    int x = player_pos.getX();
	    int y = player_pos.getY();
	    switch (direction){
	    case LEFT:
		y--;
		break;
	    case RIGHT:
		y++;
		break;
	    case UP:
		x--;
		break;
	    case DOWN:
		x++;
		break;
	    }

	    Tile nextTile = current_map.getTile(x, y);
	    Types.TileAct tile_state = nextTile.okToStep(player);

	    switch(tile_state){
	    case MOVE:
		break;
	    case OPEN:
		current_map.knockDown(x,y);
		break;

	    case ITEM:
		Types.Item item = ((NormalTile)nextTile).getItem();
		if(item == Types.Item.CHIP)
		    chips_left--;
		else
		    player.giveItem(item);
		break;

		/* clearing the moves queue stops the player from building up
		 * unwanted moves while on an ice patch or move tiles by idly
		 * poking keys*/
	    case CONT:
		moves_queue.clear();
		player.changePosition(x, y);
		try{ Thread.sleep(SLEEP); }catch(Exception e){}

		sleep_count += SLEEP;
		makeMove(direction, player_id);
		return;

	    case GOTO:
		moves_queue.clear();
		player.changePosition(x, y);
		try{ Thread.sleep(SLEEP);}catch(Exception e){}
		sleep_count += SLEEP;
		makeMove(((MoveTile)nextTile).getDirection(), player_id);
		return;

	    case END:
		if(chips_left == 0)
		    game_state = Types.EngineState.WIN;
		break;

	    case KILL:
		game_state = Types.EngineState.DEAD;
		break;

	    case STOP:
		return;
	    }

	    player.changePosition(x, y);
	}
    }

    /**
     * This method empties every player's move queue. This is used when a
     * level is first loaded, to ensure that random keyboard presses during
     * the transition don't effect play.
     */
    private void clearQueues(){
        Set<Integer> keys = queues.keySet();
        for(Integer key : keys)
            (queues.get(key)).clear();
    }
}
